package ru.common.string;

import java.io.IOException;
import java.io.Reader;

/**
 * Implements a {@link java.io.Reader} over a {@link StringBuilder} instance. Although
 * one can use {@link java.io.StringReader} by passing it
 * {@link StringBuilder#toString()}, it is better to use this class, as it
 * doesn't mark the passed-in {@link StringBuilder} as shared (which will cause
 * inner char[] allocations at the next append() attempt).<br>
 * Notes:
 * <ul>
 * <li>This implementation assumes the underlying {@link StringBuilder} is not
 * changed during the use of this {@link java.io.Reader} implementation.
 * <li>This implementation is thread-safe.
 * <li>The implementation looks very much like {@link java.io.StringReader} (for
 * the right reasons).
 * <li>If one wants to reuse that instance, then the following needs to be done:
 * <pre>
 * StringBuilder sb = new StringBuilder("some text");
 * Reader reader = new StringBuilderReader(sb);
 * ... read from reader - don't close it ! ...
 * sb.setLength(0);
 * sb.append("some new text");
 * reader.reset();
 * ... read the new string from the reader ...
 * </pre>
 * </ul>
 */
public final class StringBuilderReader extends Reader {

    // The StringBuilder to read from.
    private StringBuilder sb;

    // The length of 'sb'.
    private int length;

    // The next position to read from the StringBuilder.
    private int next = 0;

    // The mark position. The default value 0 means the start of the text.
    private int mark = 0;

    public StringBuilderReader(StringBuilder sb) {
        set(sb);
    }

    /**
     * Check to make sure that the stream has not been closed.
     */
    private void ensureOpen() throws IOException {
        if (sb == null) {
            throw new IOException("Stream has already been closed");
        }
    }

    @Override
    public void close() {
        synchronized (lock) {
            sb = null;
        }
    }

    /**
     * Mark the present position in the stream. Subsequent calls to reset() will
     * reposition the stream to this point.
     *
     * @param readAheadLimit Limit on the number of characters that may be read
     *                       while still preserving the mark. Because the stream's input comes
     *                       from a StringBuilder, there is no actual limit, so this argument
     *                       must not be negative, but is otherwise ignored.
     * @throws IllegalArgumentException If readAheadLimit is < 0
     * @throws java.io.IOException              If an I/O error occurs
     */
    @Override
    public void mark(int readAheadLimit) throws IOException {
        if (readAheadLimit < 0) {
            throw new IllegalArgumentException("Read-ahead limit cannpt be negative: " + readAheadLimit);
        }
        synchronized (lock) {
            ensureOpen();
            mark = next;
        }
    }

    @Override
    public boolean markSupported() {
        return true;
    }

    @Override
    public int read() throws IOException {
        synchronized (lock) {
            ensureOpen();
            return next >= length ? -1 : sb.charAt(next++);
        }
    }

    @Override
    public int read(char cbuf[], int off, int len) throws IOException {
        synchronized (lock) {
            ensureOpen();

            // Validate parameters
            if (off < 0 || off > cbuf.length || len < 0 || off + len > cbuf.length) {
                throw new IndexOutOfBoundsException("off=" + off + " len=" + len + " cbuf.length=" + cbuf.length);
            }

            if (len == 0) {
                return 0;
            }

            if (next >= length) {
                return -1;
            }

            int n = Math.min(length - next, len);
            sb.getChars(next, next + n, cbuf, off);
            next += n;
            return n;
        }
    }

    @Override
    public boolean ready() throws IOException {
        synchronized (lock) {
            ensureOpen();
            return true;
        }
    }

    @Override
    public void reset() throws IOException {
        synchronized (lock) {
            ensureOpen();
            next = mark;
            length = sb.length();
        }
    }

    public void set(StringBuilder sb) {
        synchronized (lock) {
            this.sb = sb;
            length = sb.length();
            next = mark = 0;
        }
    }

    @Override
    public long skip(long ns) throws IOException {
        synchronized (lock) {
            ensureOpen();
            if (next >= length) {
                return 0;
            }

            // Bound skip by beginning and end of the source
            long n = Math.min(length - next, ns);
            n = Math.max(-next, n);
            next += n;
            return n;
        }
    }

}